查看原文
其他

百万级库表能力!这个MongoDB为什么可以这么牛?

CMongo团队 云加社区 2022-06-14


导语 | 腾讯云MongoDB目前广泛应用于游戏、电商、ugc、物联网等场景,很多客户在使用过程中库表数量会大量增长,甚至达到百万级别,导致性能急剧下降,严重影响客户业务。腾讯数据库研发中心MongoDB团队在进行性能分析之后,使用了共享表空间思路,将用户创建的海量库表共享底层WT引擎的1个表空间。WT引擎维护的表数量不随用户创建表和索引的操作线性增长,始终保证在个位数。优化后的架构将读写性能提升了1-2个数量级,有效降低了内存资源消耗,实现了百万级库表能力,并将启动时间从原先的小时级缩短到1分钟内。这样的提升究竟是如何实现的?请看腾讯云MongoDB百万库表能力提升的探索之路。


探索之路正式开始,文章很长但干货满满,建议收藏品用——


一、 背景:发现问题,立项攻坚


腾讯云MongoDB(简称 CMongo)在运营过程中发现很多业务存在创建大量库表的需求,而随着业务库表数量的不断增长,客户反馈持续出现几秒到几十秒的慢查询,并伴随节点不可用的情况,严重影响到了客户的正常业务请求。

通过监控观察发现,原生MongoDB在库表和索引数量达到百万量级场景下, MongoDB实例在CPU、磁盘等资源远没有达到瓶颈时,也会出现操作卡顿性能下降的问题。


从我们的运营观察来看,至少有以下 3 个非常严重的问题:


  • 性能严重下降,慢查询变多;

  • 内存消耗增大,频繁出现OOM;

  • 实例启动时间明显变长,可能达到小时级。


针对上述问题,腾讯数据库研发中心MongoDB团队基于 v4.0 版本,对原生场景下的百万库表场景进行了性能分析,并结合业界的解决方案进行架构优化,最终取得了非常好的效果(优化后的架构在百万级库表的场景下,将读写性能提升了1-2个数量级,有效降低了内存资源消耗,并将启动时间从原先的小时级缩短到 1 分钟内。)。本文将根据MongoDB原理,为大家介绍分析过程及MongoDB架构优化方案。


二、知己知彼:原生多表场景性能分析


MongoDB内核从3.2版本开始,采用了典型的插件式架构,可以简单理解为分为server层和存储引擎层,通过打点和日志调试,我们最终发现,所有问题都指向存储引擎层,因此,WiredTiger引擎的分析成为了我们后续的重中之重。


(一)WiredTiger 存储引擎简介

MongoDB采用WiredTiger(简称WT) 作为默认存储引擎,整体架构如下图所示:


(WiredTiger 存储引擎整体架构)


用户在MongoDB层创建的每个表和索引,都对应各自独立的WT表。

数据读写经过以下3层:


1.WT Cache:通过B+树缓存未压缩的库表数据,并通过自定义的淘汰算法确保内存占用在合理范围。


2.OS Cache:由操作系统管理,缓存压缩后的库表数据。


3.数据库文件:存储压缩后的库表数据。每个WT表对应一个独立的磁盘文件。磁盘文件划分成多个按照4KB对齐的extent(offset+length),并通过3个链表来管理:available list(可分配的extent列表),discard list(废弃的extent列表,但是还不能马上重用,可能其他checkpoint还在引用)和allocate list(当前已分配的extent列表。

(二)内存消耗分析


思考:如果用户不会在短时间内访问所有的表,必然有表长时间空闲,那为何非活跃表的数据长时间停留在内存?


假设:如果非活跃表占用的内存能够及时换出,那将有效提高一个普通规格的集群能够支持的最大表数量,从而避免频繁OOM。


探索过程:我们在云上创建一个2核4G的副本集,不断创建表(每个表2个索引),每个表在插入数据创建完成之后不再访问。测试过程中发现current active dhandle一直在上升,而connection sweep dhandles closed指标却很平缓。最终实例占用的内存也一直上升,在创建的表不足1万时,就触发了实例OOM。


data handle & sweep thread
data handle(简称 dhandle) 可以简单理解为wiredtiger资源的专属句柄,类似系统的fd


在全局WT_CONNECTION对象中维护着全局的dhandle list, 每个 WT_SESSION对象维护着指向dhandle list的dhandle cache。第一次访问WT表时,在全局dhandle list和session dhandle cache中没有对应的dhandle,会为此表创建dhandle, 并放到全局dhandle list中


sweep thread后台线程每10秒扫描WT_CONNECTION中的dhandle list,标记当前没有使用的dhandle。如果打开的btree数量超过了close_handle_minimum(默认值250),则检查有哪些dhandle在 close_idle_time内一直处于idle状态,则关闭与此dhandle相关的 btree,释放一些资源(非全部资源),标记dhandle为dead以便引用它的session能够发现此dhandle已经不能再访问。接下来还得判断是否有session引用这个dead dhandle,如果没有则从全局list中移除。


基于以上分析,我们有理由怀疑为何清理的效率这么差


深入分析MongoDB代码可知,在初始化WiredTiger引擎时将close_idle_time设置成了100000s(~28h)。 这样设置导致的后果是sweep不够及时,不再访问的表仍然会长时间消耗内存。有兴趣的读者可以参与jira进行讨论。


验证得出结果:为了快速验证我们的分析,将代码中close_idle_time从原先的1万秒调成600秒,运行相同测试程序,发现节点没有OOM,1万个 collection成功插入,内存使用率也维持在较低值。


(不同close_idle_time的内存消耗图)


connection data handles currently active在一开始就不再直线上升,而是有升有降,最终程序结束后,回归到250,符合预期。所以,对于线上业务表多、频繁OOM、实例规格又小的集群,可以临时采取调整配置的方式来缓解。但是这样又会导致dhandle的缓存命中率非常低,带来比较严重的性能下降。


因此,如何降低dhandle个数减少内存消耗,同时又保证dhandle缓存命中率避免性能下降,成为了我们的重点优化目标之一

(三)读写性能分析探索过程场景一:预热前性能分析


预热指顺序读取每个collection至少一条数据,以便让所有collection的cursor都进入到session cursor cache。


在整个测试过程中,每个线程随机选择collection进行测试,可以发现持续有慢查询。通过下面的监控展示可以看到,在wait time窗口,随着蓝色曲线(schema lock wait)飙高,read latency也飙高。因此,有必要分析schema lock的使用逻辑。


(预热前随机查询监控图)


为什么简单的CRUD请求要直接或间接依赖schema lock呢?


在测试过程中生成火焰图,得到函数调用栈如下:


(预热前单点随机查询火焰图)


在"all"上方还有很多像"conn10091"线程一样的火焰柱形,表明客户端处理线程都在抢schema lock,而它是属于WT_CONNECTION对象的全局锁。这样也就解释了为何会产生那么大的schema lock wait。


那为何线程都在执行open_cursor呢?


在执行CRUD操作时,MongoServer线程需要选择一个WT_SESSION, 再从WT_SESSION中获取表的wtcursor,然后执行findRecord, insertRecord, updateRecord等操作。


为了保证性能,在MongoServer侧和WT引擎侧都会缓存wtcursor。但如果表是首次访问,此时dhandle和wtcursor还未生成,需要执行代价昂贵的创建操作,包括打开file,建立btree结构等。


结合代码和火焰图可知,open_cursor获取schema lock之后在get dhandle阶段消耗了很多CPU时间。当系统刚启动未预热时,很显然会有大量的open_cursor调用,从而造成spinlock长时间等待。可见,dhandle越多,竞争也越大。若想减小这种spinlock竞争,就需减少dhandle数量,这样就能大大地加快预热速度。这也是我们后续优化的一个攻克方向

场景二:预热后性能分析


持续读写的场景下,在经历了“数据预热”阶段之后,每个WT表的data handle都完成了open操作,此时前文描述schema lock已经不再成为性能瓶颈。但是我们发现和表少的场景相比,性能仍然相对低下。


为了进一步分析性能瓶颈,我们对读写请求的全链路各个阶段进行了分段统计和分析,发现在data handle缓存访问阶段耗时很长。


WT_CONNECTION和WT_SESSION会缓存data handle,并采用哈希链表加速查找,默认的hash bucket的个数为512。 在百万级库表场景下,每个list会变得很长,查找效率剧烈下降,从而导致用户的读写请求变慢


通过在WT代码中增加data handle缓存的访问性能统计,发现用户侧慢请求的个数和data handle访问慢操作的个数相关,而且时延分布也基本一致。如下所示:



为了快速验证我们的分析,可以在压测时尝试将Bucket个数提升100倍,使每个Hash链表的长度大幅变短,发现性能会有几倍甚至数量级的提升。


通过以上分析,可以得到的启示是:如果能够将MongoDB表共享到少量WT表空间中,能够降低data handle个数,提升data handle缓存的访问效率,从而提升性能

(四)启动速度分析

原生MongoDB在百万级库表场景下,启动mongod实例会耗时长达几十分钟,甚至超过1个小时。如果有多个节点在OOM之后不能被很快被拉起提供服务,则整体服务可用性将受到很大影响


探索过程
:为了观察启动期间MongoServer和WT引擎的流程和耗时分布,我们对MongoDB和WT引擎日志进行了分析统计


通过日志分析发现,耗时最长的部分为WT表的
reconfig阶段


具体来说,在mongod启动时mongoDbMain的初始化阶段会初始化存储引擎,并执行loadCatalog初始化所有表的WiredTigerRecordStore。在构造WiredTigerRecordStore时,会根据表的uri执行setTableLogging配置底层WT表是否开启 WAL。最后调用底层WT引擎的schema_alter流程执行配置,这里会涉及到多次IO操作(获取原有配置,再和新配置进行比较,最后可能执行配置更新)。同理,mongoDbMain 也会初始化所有表的索引(WiredTigerIndex),也会执行对应的setTableLogging操作。


另外,以上流程都是串行操作,在库表索引变多时,整体耗时也会线性增长。实测发现在百万库表场景下超过99%的耗时都在这个阶段,成为了启动速度的瓶颈。


为什么启动时要对WT表进行reconfig呢?


MongoDB会根据表的用途以及当前的配置,决定是否对某个表开启WAL,具体可以参考WiredTigerUtil::useTableLogging和WiredTigerUtil::setTableLogging的实现逻辑。在mongod实例启动以及用户建表时都会进行相关配置。仔细分析这里的逻辑,可以得到以下规律:
在表创建完成,对应的WT uri确定之后,这个表是否开启WAL是可以确定的,不会随着实例的运行发生改变。


通过以上分析,可以得到2点启示


1.如果将开启WAL配置一致的MongoDB表都共享到少量WT表空间中,可以将setTableLogging的操作次数由百万级降低为到个位数,从而极大提升初始化速度。从我们的架构优化和测试也能证明,可以将小时级的启动时间优化到1分钟内。


2.减少setTableLogging操作之后,会避免WT引擎进行schema_alter操作时获取全局schema lock,从而给MongoServer的上层逻辑带来优化空间。通过将串行初始化优化成多线程并发初始化表和索引,能够进一步加快启动速度。


(五)性能分析总结


WT引擎在百万级库表场景下,会对应生成大量的data handle并导致性能下降,包括锁竞争、关键数据结构的缓存命中率低、部分串行化流程在百万级库表下带来的耗时放大等问题。

(六)思考


结合前面的分析,如果MongoServer层能够共享表空间,只在WT引擎上存储数量较少的物理表,是否能避免上述性能瓶颈?


三、青胜于蓝:CMongo百万库表架构优化


(一)方案选型


在MySQL中,InnoDB通过为每个表分配space id实现共享表空间;在MongoRocks中, 每个表会分配唯一的prefix,每条数据的Key都会带上prefix信息。


在原生MongoDB代码中,也遵循“前缀映射”的思路实现了部分基础代码。具体可以参考KVPrefix的定义,以及GroupCollections选项的相关代码和注释。但是这个功能并没有完全实现,只进行了基础的数据结构定义,因此也不能直接使用。我们通过邮件和作者进行了沟通,社区目前也没有实现该功能的后续计划MongoDB JIRA相关信息如下:


问题
描述
当前状态
「使WT cursor 支持 groupCollections」-https://link.zhihu.com/?target=https%3A//jira.mongodb.org/browse/SERVER-28743当开启了groupCollections后,数据和索引的key_format分别变成qq, qu,cursor 在迭代时要检测prefix是否匹配fixed
「兼容sampling cursor」-https://link.zhihu.com/?target=https%3A//jira.mongodb.org/browse/WT-3341当支持groupCollections时,为 WT_CUROSR 提供一个新方法 range() 以支持random cursoropen
「兼容sampling cursor」- https://link.zhihu.com/?target=https%3A//jira.mongodb.org/browse/SERVER-29113与第2个问题相关,需要WT 层支持random cursor 正确返回匹配前缀的数据。这在为oplog 设置截断点时要用到。据原作者所说,有比较多的工作要做closed
「官方测试百万表」- https://link.zhihu.com/?target=https%3A//jira.mongodb.org/browse/WT-3337对groupCollections 特性进行压测的POC 程序closed
「官方groupCollections 设计文档」- https://link.zhihu.com/?target=https%3A//jira.mongodb.org/browse/SERVER-28745工作较多,包括共享session等-
「底层表不存在时才创建」- https://link.zhihu.com/?target=https%3A//jira.mongodb.org/browse/SERVER-28744当开启groupCollections时,是先搜索是否存在兼容的table,只能在没有underlying table时才创建它; 删除collection/indexes 要注意不要无条件删除了底层表; oplog 要单独放closed


结合我们对原生多表场景下的测试和分析,认为共享表空间可以解决上述性能瓶颈,从而提升性能。因此我们决定基于“前缀映射”的思路进行共享表空间架构优化和验证

(二)架构设计


在方案设计初期,我们对于在哪个模块实现共享表空间进行了以下对比:


1.改造WT存储引擎内部逻辑。多个逻辑WT表通过前缀进行区分,共享同一个物理WT表空间,共享data handle, btree, block manager等资源。但是这种方式涉及的代码改动量极大,开发周期太长。


2.改造MongoDB中KVEngine抽象层的逻辑。在存储引擎上层通过前缀映射的方式,使多个MongoDB表共享同一个WT表空间。这种方式主要涉及KVEngine存储抽象层使用逻辑的改造,并兼容原生WT引擎对前缀操作支持不完备带来的问题。这种方式涉及的代码改动量相对较少,不涉及WT引擎内部架构的调整,稳定性和开发周期更加可控。


因此,优化工作主要集中在“KVEngine抽象层”:通过前缀方式,将多个MongoDB用户表映射成为1个WT表;MongoServer层对指定库表的CURD操作,都会通过key -> prefix+key的转换之后,到WT引擎进行数据操作。上述映射关系通过 __mdb_catalog存储


整体架构图如下所示:


(优化后整体架构)


建立好映射关系之后,不论用户在上层创建多少个库表和索引,在WT引擎层都只会有9个WT表:


1.WiredTiger.wt:存储Checkpoint元数据信息
2.WiredTigerLAS.wt:存储换出的LAS数据
3._mdb_catalog.wt:存储上层
MongoDB表和底层WT表的映射关系
4.sizeStorer.wt:存储计数数据
5.oplog-collection.wt:存储oplog数据
6.collection.wt:存储所有
MongoDB非local表数据
7.index.wt:存储所有
MongoDB非local索引数据
8.local-collection.wt:存储
MongoDB local表数据
9.local-index.wt:存储
MongoDB local索引数据


为什么表和索引分开存储?


因为表和索引在WT引擎中的schema定义不同,比如表的schema是qq -> u,索引的schema是qu -> q(q表示int64, u表示string);也可以将collection和index的schema都统一成qu -> u的形式, 但是在数据读写时会带来一些额外的类型转换。


每一个表都对应一个prefix,通过建立(NS, Prefix, Ident)三元关系来将多个表的数据共享到一个文件中,共享后的数据文件如下所示:


(共享后数据文件)


经过架构调整之后,一些关键路径发生了变化:


路径变化1:建表和索引操作,会先生成唯一的prefix,并记录到mdb_catalog中,而不再对WT引擎发起表创建操作(WT引擎的共享表会在实例第1次启动时检查并创建完成。)。


路径变化2:删表和索引操作,会删除mdb_catalog中的记录,然后进行数据删除操作,但不会直接删除WT表文件。


路径变化3:数据读写操作,会将prefix添加到访问的key头部,然后再去WT中执行数据操作。


通过建立prefix映射关系,不论MongoDB上层库表个数如何增长,底层WT表个数都不会增长。因此,上文分析的data handle过多导致内存使用率高,data handle open操作导致的锁争抢,data handle cache效率低下,以及WT表太多导致实例启动慢的问题都不会出现,极大提升了整体性能


当然,通过prefix共享表空间之后也会带来一些新的使用限制,主要有:


  • 用户删除表之后,空间不会释放,但是可以被重新使用;

  • 部分表操作的语义发生了变化。比如compact和validate操作需要放到全局实现,目前暂不支持;

  • 部分统计信息发生了变化。比如表和索引的storageSize都无法统计,只能统计出逻辑大小(压缩前的大小)。由于原生sizeStorer.wt中只记录了表的逻辑大小,因此需要自己实现索引逻辑大小的统计。进行这个改造之后,show dbs(listDatabases命令)的速度提升了不少,从我们的测试结果来看可以从11s缩短到0.8s, 主要得益于不需要对大量索引文件执行统计操作。


(三)优化效果


我们在腾讯上搭建的测试环境,具体的测试环境如下:


1.数据量大小500GB(500 000 000 000 B)(collection*
recordNum

*fieldLength);

2.总体压测线程数量200;
3.副本集的配置:8核,16G内存,2T磁盘;
4.mongo driver 版本:go-driver[v1.5.2];
5.driver的连接池大小为200;
6.测试工具与primary在同一台机器上,直连primary。


测试结果


腾讯数据库研发中心MongoDB团队根据业务使用情况,挑选出50w表100w表两种场景进行测试。在实际测试过程中发现,改造之前的集群无法写入100w表的数据。最后给出50w表的测试数据。


测试的大概步骤如下所示:


1.默认构建10个库;
2.每个库先创建5w空表;
3.开始向表中写入固定数量的随机数据;
4.执行CRUD的操作。

下面给出百分位延迟对比结果图(由于数据差异较大,为了能显示对比,对所有的数据都做了log10()处理)。从P99图可以看出,改造后在CRUD操作上的性能要优于改造前。下图展示了QPS对比结果:


(改造前后QPS对比图)


改造后的QPS也远优于改造前,原因是,改造后可以认为所有的数据同在一张表中,性能不会随着表的数量发生很大的改变;而改造前抢锁的激烈程度,data handle的访问时间会随着表数量的增加而增加,由此导致性能很低


下图给出了改造前后多表以及
原生单表query操作的QPS对比图,改造后相对于原生单表的QPS最大降幅在7%以内,而改造前相对于原生单表的QPS最大降幅已经超过90%。


(QPS变化图)


线上效果


原生MongoDB随着表数量的大量增长,资源消耗也会大幅增加,性能急剧下降。腾讯数据库研发中心MongoDB团队在进行性能分析之后,使用了共享表空间思路,将用户创建的海量库表共享底层 WT 引擎的1个表空间。WT引擎维护的表数量不随用户创建表和索引的操作线性增长,始终保证在个位数。


优化后的架构在百万级库表的场景下,将读写性能提升了1-2个数量级,有效降低了内存资源消耗,并将启动时间从原先的小时级缩短到1分钟内。目前架构优化已经通过了各项功能测试和性能压测并顺利上线。云上的即时通信IM业务,替换了百万库表版本之后,有效的解决了客户表很多时造成的CRUD操作变慢的问题,系统内存消耗也明显降低。后面该内核特性也会全面开放到云上,欢迎大家体验。


四、步履不停,一直在路上


腾讯云MongoDB(TencentDB for MongoDB是腾讯基于全球广受欢迎的文档数据库MongoDB打造的高性能NoSQL数据库,100%完全兼容MongoDB协议。相比原生版本,腾讯数据库研发中心MongoDB团队对原生内核做了大量优化和深度定制,包括百万TPS、物理备份、免密、无损加节点、rocksdb引擎等优化,同时也新开发了流控、审计、加密等企业级特性,为公司内外客户的核心业务提供了有力的支撑。


目前,腾讯MongoDB已在稳定性、安全性、性能以及企业级特性上取得了显著突破。接下来我们还会继续在性能、新特性、成本等方面进行持续不断的打磨、改进,争取为公司内外用户提供更好的云MongoDB服务。


推荐阅读


Serverless 在大厂都怎么用?一文说尽Golang单元测试实战的那些事儿
前以色列国防军安全技术成员教你做好 Serverless 追踪
从万物互联到万物智联,物联网的下一个爆发点在哪里?






👇 想看看百万级库表能力的数据库到底是什么?点击【阅读全文】进入【腾讯云官网】云数据库TencentDB for MongoDB查看详情!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存